﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

enum ResType
{
    normal,
    sprite,
}

class LoadTask
{
    public string path;
    public int priority;
    public ResType resType;
    public HashSet<System.Action<object, string>> callBacks = new HashSet<System.Action<object, string>>();
}

class Comparer: IComparer<int>
{
    public int Compare(int a, int b)
    {
        return b - a;
    }
}

public static class ResourceManager
{
    public static System.Func<string, System.Type, object> externalResourceLoader;

    public const int default_priority = 10000;
    static Dictionary<System.Type, string[]> typeExtMap = new Dictionary<System.Type, string[]>();
    static string[] allExts = null;

    const int maxAsyncLoad = 20;
    static SortedDictionary<int, LinkedList<LoadTask>> waitTaskMap = new SortedDictionary<int, LinkedList<LoadTask>>(new Comparer());
    static Dictionary<string, LoadTask> pathTaskMap = new Dictionary<string, LoadTask>();
    static LinkedList<LoadTask> runTasks = new LinkedList<LoadTask>();
    static Dictionary<string, System.WeakReference> loadedAssets = new Dictionary<string, System.WeakReference>();

    public static string[] texExts = new string[]{".jpg", ".jpeg", ".png", ".tga"};

    static ResourceManager()
    {
        HashSet<string> allExtSet = new HashSet<string>();
        {
            string[] objExts = new string[]{ ".prefab", ".fbx", ".asset" };
            typeExtMap.Add(typeof(Object), objExts);
            typeExtMap.Add(typeof(GameObject), objExts);
            allExtSet.Add(objExts);
        }
        {
            string[] objExts = new string[]{ ".mat" };
            typeExtMap.Add(typeof(Material), objExts);
            allExtSet.Add(objExts);
        }
        {
            string[] objExts = new string[]{ ".bytes", ".txt" };
            typeExtMap.Add(typeof(TextAsset), objExts);
            allExtSet.Add(objExts);
        }
        {
            string[] objExts = texExts;
            typeExtMap.Add(typeof(Sprite), objExts);
            typeExtMap.Add(typeof(Texture), objExts);
            typeExtMap.Add(typeof(Texture2D), objExts);
            allExtSet.Add(objExts);
        }
        {
            string[] objExts = new string[]{".mp3"};
            typeExtMap.Add(typeof(AudioClip), objExts);
            allExtSet.Add(objExts);
        }

        allExts = allExtSet.ToArray();
    }

    public static void UnloadUnusedAssets()
    {
        Resources.UnloadUnusedAssets();
    }

    public static void Reset()
    {
        waitTaskMap.Clear();
        pathTaskMap.Clear();
        runTasks.Clear();
        loadedAssets.Clear();
    }

    public static void LoadScene(string sceneName)
    {
        ResourcesEx.LoadScene(sceneName);
    }

    public static void LoadScene(string sceneName, LoadSceneMode mode)
    {
        ResourcesEx.LoadScene(sceneName, mode);
    }

    public static AsyncOperationEx LoadSceneAsync(string sceneName)
    {
        return ResourcesEx.LoadSceneAsync(sceneName, default_priority);
    }

    public static AsyncOperationEx LoadSceneAsync(string sceneName, LoadSceneMode mode)
    {
        return ResourcesEx.LoadSceneAsync(sceneName, mode, default_priority);
    }

    public static Shader LoadShader(string shaderName)
    {
        return ResourcesEx.LoadShader(shaderName);
    }

    public static T LoadAsset<T>(string path) where T: UnityEngine.Object
    {
        return LoadAsset(path, typeof(T)) as T;
    }

    public static Object LoadAsset(string path)
	{
        return LoadAsset(path, null);
	}

	public static Object LoadAsset(string path, System.Type t)
	{
        Object asset = ResourcesEx.LoadAsset(path, t);
        if (asset != null)
        {
            return asset;
        }

        return LoadAssetByEditor(path, t);
	}
    
    public static string LoadString(string path, bool autoUnload = false)
    {
        if(externalResourceLoader != null)
        {
            object obj = externalResourceLoader(path, typeof(string));
            if(obj != null)
            {
                return obj as string;
            }
        }

        TextAsset asset = LoadAsset<TextAsset>(path);
        if(asset != null)
        {
            string ret = asset.text;
            if(autoUnload)
            {
                Resources.UnloadAsset(asset);
            }
            return ret;
        }

        return null;
    }

    public static byte[] LoadBytes(string path, bool autoUnload = false)
    {
        if(externalResourceLoader != null)
        {
            object obj = externalResourceLoader(path, typeof(byte[]));
            if(obj != null)
            {
                return obj as byte[];
            }
        }

        TextAsset asset = LoadAsset<TextAsset>(path);
        if(asset != null)
        {
            byte[] ret = asset.bytes;
            if(autoUnload)
            {
                Resources.UnloadAsset(asset);
            }
            return ret;
        }

        return null;
    }

    public static SpriteEx LoadSpriteEx(string path)
    {
        SpriteEx spriteEx = ResourcesEx.LoadSpriteEx(path);
        if (spriteEx == null)
        {
            Sprite sprite = LoadAssetByEditor<Sprite>(path);
            if (sprite != null)
            {
                spriteEx = new SpriteEx();
                spriteEx.sprite = sprite;
            }
        }

        return spriteEx;
    }

    public static void LoadSpriteExAsync(string path, System.Action<object, string> callback)
    {
        LoadSpriteExAsync(path, default_priority, callback);
    }

    public static void LoadSpriteExAsync(string path, int priority, System.Action<object, string> callback)
    {
        RealLoadAssetAsync(path, priority, callback, ResType.sprite);
    }

    public static void LoadAssetAsync(string path, System.Action<object, string> callback)
    {
        LoadAssetAsync(path, default_priority, callback);
    }

    public static void LoadAssetAsync(string path, int priority, System.Action<object, string> callback)
    {
        RealLoadAssetAsync(path, priority, callback, ResType.normal);
    }

    public static Object GetLoadedAsset(string path)
    {
        if (string.IsNullOrEmpty(path))
        {
            return null;
        }

        System.WeakReference reference = null;
        if (loadedAssets.TryGetValue(path, out reference))
        {
            if (reference.Target != null)
            {
                Object asset = reference.Target as Object;
                if (asset != null)
                {
                    return asset;
                }
            }
        }

        return null;
    }

    public static void CancelAsyncTask(string path, System.Action<object, string> callback)
    {
        if (string.IsNullOrEmpty(path))
        {
            return;
        }

        LoadTask task = GetTask(path);
        if (task != null)
        {
            task.callBacks.Remove(callback);
            if (task.callBacks.Count <= 0)
            {
                LinkedList<LoadTask> list = null;
                if (waitTaskMap.TryGetValue(task.priority, out list))
                {
                    list.Remove(task);
                    if (list.Count <= 0)
                    {
                        waitTaskMap.Remove(task.priority);
                    }

                    pathTaskMap.Remove(path);
                }
            }
        }
    }

    static void RealLoadAssetAsync(string path, int priority, System.Action<object, string> callback, ResType resType)
    {
        if (string.IsNullOrEmpty(path))
        {
            callback(null, path);
            return;
        }

        System.WeakReference reference = null;
        if (loadedAssets.TryGetValue(path, out reference))
        {
            if (reference.Target != null)
            {
                Object asset = reference.Target as Object;
                if (asset != null)
                {
                    callback(asset, path);
                    return;
                }
            }
        }

        LoadTask task = GetTask(path);
        if (task != null)
        {
            task.callBacks.Add(callback);
            return;
        }

        task = new LoadTask();
        task.priority = priority;
        task.resType = resType;
        task.path = path;
        task.callBacks.Add(callback);

        pathTaskMap.Add(path, task);

        if (runTasks.Count < maxAsyncLoad)
        {
            runTasks.AddLast(task);
            MyEventObject.instance.StartCoroutine(CheckAsyncRequest(task));
        }
        else
        {
            LinkedList<LoadTask> waitList = null;
            if (!waitTaskMap.TryGetValue(task.priority, out waitList))
            {
                waitList = new LinkedList<LoadTask>();
                waitTaskMap.Add(task.priority, waitList);
            }

            waitList.AddFirst(task);
        }
    }

    static T LoadAssetByEditor<T>(string assetPath) where T: UnityEngine.Object
    {
        return LoadAssetByEditor(assetPath, typeof(T)) as T;
    }

    static Object LoadAssetByEditor(string assetPath, System.Type type = null)
    {
        #if UNITY_EDITOR

        string[] exts = allExts;
        if(type != null)
        {
            if(!typeExtMap.TryGetValue(type, out exts))
            {
                throw new System.Exception("not found exts for type: " + type);
            }
        }
        
        for(int i = 0; i < exts.Length; i++)
        {
            if(type != null)
            {
                Object asset = UnityEditor.AssetDatabase.LoadAssetAtPath(ResourcesEx.BundleResDir + "/" + assetPath + exts[i], type);
                if (asset != null)
                {
                    return asset;
                }

                asset = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/" + assetPath + exts[i], type);
                if (asset != null)
                {
                    return asset;
                }
            }
            else
            {
                Object asset = UnityEditor.AssetDatabase.LoadMainAssetAtPath(ResourcesEx.BundleResDir + "/" + assetPath + exts[i]);
                if (asset != null)
                {
                    return asset;
                }

                asset = UnityEditor.AssetDatabase.LoadMainAssetAtPath("Assets/" + assetPath + exts[i]);
                if (asset != null)
                {
                    return asset;
                }
            }
        }
        #endif

        return null;
    }

    static IEnumerator CheckAsyncRequest(LoadTask task)
    {
//        yield return new WaitForSeconds(2);

        bool returned = false;
        if (string.IsNullOrEmpty(task.path))
        {
            returned = true;
            LoadFinish(task, null);
        }

        if (!returned)
        {
//            System.WeakReference reference = null;
//            if (loadedObjs.TryGetValue(task.path, out reference))
//            {
//                if (reference.Target != null)
//                {
//                    Object asset = reference.Target as Object;
//                    if (asset != null)
//                    {
//                        returned = true;
//                        LoadFinish(task, asset, false);
//                    }
//                }
//            }

            if (!returned)
            {
                if (task.resType == ResType.sprite)
                {
                    ResourcesEx.LoadSpriteExAsync(task.path, task.priority, (spriteEx) =>
                        {
                            if (spriteEx == null)
                            {
                                Sprite sprite = LoadAssetByEditor<Sprite>(task.path);
                                if (sprite != null)
                                {
                                    spriteEx = new SpriteEx();
                                    spriteEx.sprite = sprite;
                                }
                            }
            
                            LoadFinish(task, spriteEx, true);
                        });
                }
                else if (task.resType == ResType.normal)
                {
                    ResourcesEx.LoadAssetAsync(task.path, task.priority, (obj) =>
                        {
                            if (obj != null)
                            {
                                returned = true;
                                LoadFinish(task, obj, true);
                            }
                            else
                            {
                                if (!returned)
                                {
                                    Object asset = LoadAssetByEditor(task.path);
                                    if (asset != null)
                                    {
                                        returned = true;
                                        LoadFinish(task, asset, true);
                                    }
                                }

                                if (!returned)
                                {
                                    LoadFinish(task, null);
                                }
                            }
                        });
                }
            }
        }

        yield return null;
    }

    static LoadTask GetTask(string path)
    {
        LoadTask task = null;
        pathTaskMap.TryGetValue(path, out task);
        return task;
    }

    static void LoadFinish(LoadTask task, object asset, bool cacheAsset = true)
    {
        int oldCount = runTasks.Count;
        runTasks.Remove(task);
        pathTaskMap.Remove(task.path);

//        if(oldCount == runTasks.Count)
//        {
//            Debug.LogError("load finish error: same count: " + oldCount + "  " + runTasks.Count);
//        }

        if (cacheAsset && !string.IsNullOrEmpty(task.path))
        {
            loadedAssets.Remove(task.path);

            if (asset != null)
            {
                loadedAssets.Add(task.path, new System.WeakReference(asset));
            }
        }
        
        HashSet<System.Action<object, string>>.Enumerator e = task.callBacks.GetEnumerator();
        while (e.MoveNext())
        {
            e.Current(asset, task.path);
        }

        LinkedListNode<LoadTask> foundTask = null;
        SortedDictionary<int, LinkedList<LoadTask>>.Enumerator e2 = waitTaskMap.GetEnumerator();
        while(e2.MoveNext())
        {
            if (e2.Current.Value.Count > 0)
            {
                foundTask = e2.Current.Value.First;
                e2.Current.Value.RemoveFirst();

                if (e2.Current.Value.Count <= 0)
                {
                    waitTaskMap.Remove(e2.Current.Key);
                }

                break;
            }
        }

        if (foundTask != null)
        {
            runTasks.AddLast(foundTask);
            MyEventObject.instance.StartCoroutine(CheckAsyncRequest(runTasks.Last.Value));
        }
    }
}
